home *** CD-ROM | disk | FTP | other *** search
/ Maximum CD 2009 May / maximum-cd-2009-05.iso / DiscContents / Firefox Setup 3.0.6.exe / nonlocalized / modules / utils.js < prev    next >
Encoding:
JavaScript  |  2009-01-19  |  59.3 KB  |  1,679 lines

  1. /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  2. /* ***** BEGIN LICENSE BLOCK *****
  3.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4.  *
  5.  * The contents of this file are subject to the Mozilla Public License Version
  6.  * 1.1 (the "License"); you may not use this file except in compliance with
  7.  * the License. You may obtain a copy of the License at
  8.  * http://www.mozilla.org/MPL/
  9.  *
  10.  * Software distributed under the License is distributed on an "AS IS" basis,
  11.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12.  * for the specific language governing rights and limitations under the
  13.  * License.
  14.  *
  15.  * The Original Code is the Places Command Controller.
  16.  *
  17.  * The Initial Developer of the Original Code is Google Inc.
  18.  * Portions created by the Initial Developer are Copyright (C) 2005
  19.  * the Initial Developer. All Rights Reserved.
  20.  *
  21.  * Contributor(s):
  22.  *   Ben Goodger <beng@google.com>
  23.  *   Myk Melez <myk@mozilla.org>
  24.  *   Asaf Romano <mano@mozilla.com>
  25.  *   Sungjoon Steve Won <stevewon@gmail.com>
  26.  *   Dietrich Ayala <dietrich@mozilla.com>
  27.  *
  28.  * Alternatively, the contents of this file may be used under the terms of
  29.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  30.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  31.  * in which case the provisions of the GPL or the LGPL are applicable instead
  32.  * of those above. If you wish to allow use of your version of this file only
  33.  * under the terms of either the GPL or the LGPL, and not to allow others to
  34.  * use your version of this file under the terms of the MPL, indicate your
  35.  * decision by deleting the provisions above and replace them with the notice
  36.  * and other provisions required by the GPL or the LGPL. If you do not delete
  37.  * the provisions above, a recipient may use your version of this file under
  38.  * the terms of any one of the MPL, the GPL or the LGPL.
  39.  *
  40.  * ***** END LICENSE BLOCK ***** */
  41.  
  42. function LOG(str) {
  43.   dump("*** " + str + "\n");
  44. }
  45.  
  46. var EXPORTED_SYMBOLS = ["PlacesUtils"];
  47.  
  48. var Ci = Components.interfaces;
  49. var Cc = Components.classes;
  50. var Cr = Components.results;
  51.  
  52. const POST_DATA_ANNO = "bookmarkProperties/POSTData";
  53. const READ_ONLY_ANNO = "placesInternal/READ_ONLY";
  54. const LMANNO_FEEDURI = "livemark/feedURI";
  55. const LMANNO_SITEURI = "livemark/siteURI";
  56.  
  57. //@line 62 "e:\fx19rel\WINNT_5.2_Depend\mozilla\toolkit\components\places\src\utils.js"
  58. // On other platforms, the transferable system converts "\r\n" to "\n".
  59. const NEWLINE = "\r\n";
  60. //@line 65 "e:\fx19rel\WINNT_5.2_Depend\mozilla\toolkit\components\places\src\utils.js"
  61.  
  62. function QI_node(aNode, aIID) {
  63.   var result = null;
  64.   try {
  65.     result = aNode.QueryInterface(aIID);
  66.   }
  67.   catch (e) {
  68.   }
  69.   return result;
  70. }
  71. function asVisit(aNode)    { return QI_node(aNode, Ci.nsINavHistoryVisitResultNode);    }
  72. function asFullVisit(aNode){ return QI_node(aNode, Ci.nsINavHistoryFullVisitResultNode);}
  73. function asContainer(aNode){ return QI_node(aNode, Ci.nsINavHistoryContainerResultNode);}
  74. function asQuery(aNode)    { return QI_node(aNode, Ci.nsINavHistoryQueryResultNode);    }
  75.  
  76. var PlacesUtils = {
  77.   // Place entries that are containers, e.g. bookmark folders or queries.
  78.   TYPE_X_MOZ_PLACE_CONTAINER: "text/x-moz-place-container",
  79.   // Place entries that are bookmark separators.
  80.   TYPE_X_MOZ_PLACE_SEPARATOR: "text/x-moz-place-separator",
  81.   // Place entries that are not containers or separators
  82.   TYPE_X_MOZ_PLACE: "text/x-moz-place",
  83.   // Place entries in shortcut url format (url\ntitle)
  84.   TYPE_X_MOZ_URL: "text/x-moz-url",
  85.   // Place entries formatted as HTML anchors
  86.   TYPE_HTML: "text/html",
  87.   // Place entries as raw URL text
  88.   TYPE_UNICODE: "text/unicode",
  89.  
  90.   /**
  91.    * The Bookmarks Service.
  92.    */
  93.   get bookmarks() {
  94.     delete this.bookmarks;
  95.     return this.bookmarks = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
  96.                             getService(Ci.nsINavBookmarksService);
  97.   },
  98.  
  99.   /**
  100.    * The Nav History Service.
  101.    */
  102.   get history() {
  103.     delete this.history;
  104.     return this.history = Cc["@mozilla.org/browser/nav-history-service;1"].
  105.                           getService(Ci.nsINavHistoryService);
  106.   },
  107.  
  108.   /**
  109.    * The Live Bookmark Service.
  110.    */
  111.   get livemarks() {
  112.     delete this.livemarks;
  113.     return this.livemarks = Cc["@mozilla.org/browser/livemark-service;2"].
  114.                             getService(Ci.nsILivemarkService);
  115.   },
  116.  
  117.   /**
  118.    * The Annotations Service.
  119.    */
  120.   get annotations() {
  121.     delete this.annotations;
  122.     return this.annotations = Cc["@mozilla.org/browser/annotation-service;1"].
  123.                               getService(Ci.nsIAnnotationService);
  124.   },
  125.  
  126.   /**
  127.    * The Favicons Service
  128.    */
  129.   get favicons() {
  130.     delete this.favicons;
  131.     return this.favicons = Cc["@mozilla.org/browser/favicon-service;1"].
  132.                            getService(Ci.nsIFaviconService);
  133.   },
  134.  
  135.   /**
  136.    * The Places Tagging Service
  137.    */
  138.   get tagging() {
  139.     delete this.tagging;
  140.     return this.tagging = Cc["@mozilla.org/browser/tagging-service;1"].
  141.                           getService(Ci.nsITaggingService);
  142.   },
  143.  
  144.   /**
  145.    * Makes a URI from a spec.
  146.    * @param   aSpec
  147.    *          The string spec of the URI
  148.    * @returns A URI object for the spec.
  149.    */
  150.   _uri: function PU__uri(aSpec) {
  151.     return Cc["@mozilla.org/network/io-service;1"].
  152.            getService(Ci.nsIIOService).
  153.            newURI(aSpec, null, null);
  154.   },
  155.  
  156.   /**
  157.    * String bundle helpers
  158.    */
  159.   get _bundle() {
  160.     const PLACES_STRING_BUNDLE_URI =
  161.         "chrome://places/locale/places.properties";
  162.     delete this._bundle;
  163.     return this._bundle = Cc["@mozilla.org/intl/stringbundle;1"].
  164.                           getService(Ci.nsIStringBundleService).
  165.                           createBundle(PLACES_STRING_BUNDLE_URI);
  166.   },
  167.  
  168.   getFormattedString: function PU_getFormattedString(key, params) {
  169.     return this._bundle.formatStringFromName(key, params, params.length);
  170.   },
  171.  
  172.   getString: function PU_getString(key) {
  173.     return this._bundle.GetStringFromName(key);
  174.   },
  175.  
  176.   /**
  177.    * Determines whether or not a ResultNode is a Bookmark folder.
  178.    * @param   aNode
  179.    *          A result node
  180.    * @returns true if the node is a Bookmark folder, false otherwise
  181.    */
  182.   nodeIsFolder: function PU_nodeIsFolder(aNode) {
  183.     return (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
  184.             aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT);
  185.   },
  186.  
  187.   /**
  188.    * Determines whether or not a ResultNode represents a bookmarked URI.
  189.    * @param   aNode
  190.    *          A result node
  191.    * @returns true if the node represents a bookmarked URI, false otherwise
  192.    */
  193.   nodeIsBookmark: function PU_nodeIsBookmark(aNode) {
  194.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI &&
  195.            aNode.itemId != -1;
  196.   },
  197.  
  198.   /**
  199.    * Determines whether or not a ResultNode is a Bookmark separator.
  200.    * @param   aNode
  201.    *          A result node
  202.    * @returns true if the node is a Bookmark separator, false otherwise
  203.    */
  204.   nodeIsSeparator: function PU_nodeIsSeparator(aNode) {
  205.  
  206.     return (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR);
  207.   },
  208.  
  209.   /**
  210.    * Determines whether or not a ResultNode is a visit item.
  211.    * @param   aNode
  212.    *          A result node
  213.    * @returns true if the node is a visit item, false otherwise
  214.    */
  215.   nodeIsVisit: function PU_nodeIsVisit(aNode) {
  216.     const NHRN = Ci.nsINavHistoryResultNode;
  217.     var type = aNode.type;
  218.     return type == NHRN.RESULT_TYPE_VISIT ||
  219.            type == NHRN.RESULT_TYPE_FULL_VISIT;
  220.   },
  221.  
  222.   /**
  223.    * Determines whether or not a ResultNode is a URL item.
  224.    * @param   aNode
  225.    *          A result node
  226.    * @returns true if the node is a URL item, false otherwise
  227.    */
  228.   uriTypes: [Ci.nsINavHistoryResultNode.RESULT_TYPE_URI,
  229.              Ci.nsINavHistoryResultNode.RESULT_TYPE_VISIT,
  230.              Ci.nsINavHistoryResultNode.RESULT_TYPE_FULL_VISIT],
  231.   nodeIsURI: function PU_nodeIsURI(aNode) {
  232.     return this.uriTypes.indexOf(aNode.type) != -1;
  233.   },
  234.  
  235.   /**
  236.    * Determines whether or not a ResultNode is a Query item.
  237.    * @param   aNode
  238.    *          A result node
  239.    * @returns true if the node is a Query item, false otherwise
  240.    */
  241.   nodeIsQuery: function PU_nodeIsQuery(aNode) {
  242.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
  243.   },
  244.  
  245.   /**
  246.    * Determines if a node is read only (children cannot be inserted, sometimes
  247.    * they cannot be removed depending on the circumstance)
  248.    * @param   aNode
  249.    *          A result node
  250.    * @returns true if the node is readonly, false otherwise
  251.    */
  252.   nodeIsReadOnly: function PU_nodeIsReadOnly(aNode) {
  253.     if (this.nodeIsFolder(aNode))
  254.       return this.bookmarks.getFolderReadonly(asQuery(aNode).folderItemId);
  255.     if (this.nodeIsQuery(aNode) &&
  256.         asQuery(aNode).queryOptions.resultType !=
  257.           Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS)
  258.       return aNode.childrenReadOnly;
  259.     return false;
  260.   },
  261.  
  262.   /**
  263.    * Determines whether or not a ResultNode is a host container.
  264.    * @param   aNode
  265.    *          A result node
  266.    * @returns true if the node is a host container, false otherwise
  267.    */
  268.   nodeIsHost: function PU_nodeIsHost(aNode) {
  269.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
  270.            aNode.parent &&
  271.            asQuery(aNode.parent).queryOptions.resultType ==
  272.              Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY;
  273.   },
  274.  
  275.   /**
  276.    * Determines whether or not a ResultNode is a day container.
  277.    * @param   node
  278.    *          A NavHistoryResultNode
  279.    * @returns true if the node is a day container, false otherwise
  280.    */
  281.   nodeIsDay: function PU_nodeIsDay(aNode) {
  282.     var resultType;
  283.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
  284.            aNode.parent &&
  285.            ((resultType = asQuery(aNode.parent).queryOptions.resultType) ==
  286.                Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
  287.              resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY);
  288.   },
  289.  
  290.   /**
  291.    * Determines whether or not a result-node is a tag container.
  292.    * @param   aNode
  293.    *          A result-node
  294.    * @returns true if the node is a tag container, false otherwise
  295.    */
  296.   nodeIsTagQuery: function PU_nodeIsTagQuery(aNode) {
  297.     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
  298.            asQuery(aNode).queryOptions.resultType ==
  299.              Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS;
  300.   },
  301.  
  302.   /**
  303.    * Determines whether or not a ResultNode is a container.
  304.    * @param   aNode
  305.    *          A result node
  306.    * @returns true if the node is a container item, false otherwise
  307.    */
  308.   containerTypes: [Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
  309.                    Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
  310.                    Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY,
  311.                    Ci.nsINavHistoryResultNode.RESULT_TYPE_DYNAMIC_CONTAINER],
  312.   nodeIsContainer: function PU_nodeIsContainer(aNode) {
  313.     return this.containerTypes.indexOf(aNode.type) != -1;
  314.   },
  315.  
  316.   /**
  317.    * Determines whether or not a ResultNode is an history related container.
  318.    * @param   node
  319.    *          A result node
  320.    * @returns true if the node is an history related container, false otherwise
  321.    */
  322.   nodeIsHistoryContainer: function PU_nodeIsHistoryContainer(aNode) {
  323.     var resultType;
  324.     return this.nodeIsQuery(aNode) &&
  325.            ((resultType = asQuery(aNode).queryOptions.resultType) ==
  326.               Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY ||
  327.             resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
  328.             resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY ||
  329.             this.nodeIsDay(aNode) ||
  330.             this.nodeIsHost(aNode));
  331.   },
  332.  
  333.   /**
  334.    * Determines whether or not a result-node is a dynamic-container item.
  335.    * The dynamic container result node type is for dynamically created
  336.    * containers (e.g. for the file browser service where you get your folders
  337.    * in bookmark menus).
  338.    * @param   aNode
  339.    *          A result node
  340.    * @returns true if the node is a dynamic container item, false otherwise
  341.    */
  342.   nodeIsDynamicContainer: function PU_nodeIsDynamicContainer(aNode) {
  343.     if (aNode.type == NHRN.RESULT_TYPE_DYNAMIC_CONTAINER)
  344.       return true;
  345.     return false;
  346.   },
  347.  
  348.  /**
  349.   * Determines whether a result node is a remote container registered by the
  350.   * livemark service.
  351.   * @param aNode
  352.   *        A result Node
  353.   * @returns true if the node is a livemark container item
  354.   */
  355.   nodeIsLivemarkContainer: function PU_nodeIsLivemarkContainer(aNode) {
  356.     // Use the annotations service directly to avoid instantiating
  357.     // the Livemark service on startup. (bug 398300)
  358.     return this.nodeIsFolder(aNode) &&
  359.            this.annotations.itemHasAnnotation(aNode.itemId, LMANNO_FEEDURI);
  360.   },
  361.  
  362.  /**
  363.   * Determines whether a result node is a live-bookmark item
  364.   * @param aNode
  365.   *        A result node
  366.   * @returns true if the node is a livemark container item
  367.   */
  368.   nodeIsLivemarkItem: function PU_nodeIsLivemarkItem(aNode) {
  369.     return aNode.parent && this.nodeIsLivemarkContainer(aNode.parent);
  370.   },
  371.  
  372.   /**
  373.    * Determines whether or not a node is a readonly folder.
  374.    * @param   aNode
  375.    *          The node to test.
  376.    * @returns true if the node is a readonly folder.
  377.   */
  378.   isReadonlyFolder: function(aNode) {
  379.     return this.nodeIsFolder(aNode) &&
  380.            this.bookmarks.getFolderReadonly(asQuery(aNode).folderItemId);
  381.   },
  382.  
  383.   /**
  384.    * Gets the concrete item-id for the given node. Generally, this is just
  385.    * node.itemId, but for folder-shortcuts that's node.folderItemId.
  386.    */
  387.   getConcreteItemId: function PU_getConcreteItemId(aNode) {
  388.     if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT)
  389.       return asQuery(aNode).folderItemId;
  390.     else if (PlacesUtils.nodeIsTagQuery(aNode)) {
  391.       // RESULTS_AS_TAG_CONTENTS queries are similar to folder shortcuts
  392.       // so we can still get the concrete itemId for them.
  393.       var queries = aNode.getQueries({});
  394.       var folders = queries[0].getFolders({});
  395.       return folders[0];
  396.     }
  397.     return aNode.itemId;
  398.   },
  399.  
  400.   /**
  401.    * Gets the index of a node within its parent container
  402.    * @param   aNode
  403.    *          The node to look up
  404.    * @returns The index of the node within its parent container, or -1 if the
  405.    *          node was not found or the node specified has no parent.
  406.    */
  407.   getIndexOfNode: function PU_getIndexOfNode(aNode) {
  408.     var parent = aNode.parent;
  409.     if (!parent)
  410.       return -1;
  411.     var wasOpen = parent.containerOpen;
  412.     var result, oldViewer;
  413.     if (!wasOpen) {
  414.       result = parent.parentResult;
  415.       oldViewer = result.viewer;
  416.       result.viewer = null;
  417.       parent.containerOpen = true;
  418.     }
  419.     var cc = parent.childCount;
  420.     for (var i = 0; i < cc && parent.getChild(i) != aNode; ++i);
  421.     if (!wasOpen) {
  422.       parent.containerOpen = false;
  423.       result.viewer = oldViewer;
  424.     }
  425.     return i < cc ? i : -1;
  426.   },
  427.  
  428.   /**
  429.    * String-wraps a result node according to the rules of the specified
  430.    * content type.
  431.    * @param   aNode
  432.    *          The Result node to wrap (serialize)
  433.    * @param   aType
  434.    *          The content type to serialize as
  435.    * @param   [optional] aOverrideURI
  436.    *          Used instead of the node's URI if provided.
  437.    *          This is useful for wrapping a container as TYPE_X_MOZ_URL,
  438.    *          TYPE_HTML or TYPE_UNICODE.
  439.    * @param   aForceCopy
  440.    *          Does a full copy, resolving folder shortcuts.
  441.    * @returns A string serialization of the node
  442.    */
  443.   wrapNode: function PU_wrapNode(aNode, aType, aOverrideURI, aForceCopy) {
  444.     var self = this;
  445.  
  446.     // when wrapping a node, we want all the items, even if the original
  447.     // query options are excluding them.
  448.     // this can happen when copying from the left hand pane of the bookmarks
  449.     // organizer
  450.     function convertNode(cNode) {
  451.       if (self.nodeIsFolder(cNode) && asQuery(cNode).queryOptions.excludeItems) {
  452.         var concreteId = self.getConcreteItemId(cNode);
  453.         return self.getFolderContents(concreteId, false, true).root;
  454.       }
  455.       return cNode;
  456.     }
  457.  
  458.     switch (aType) {
  459.       case this.TYPE_X_MOZ_PLACE:
  460.       case this.TYPE_X_MOZ_PLACE_SEPARATOR:
  461.       case this.TYPE_X_MOZ_PLACE_CONTAINER:
  462.         var writer = {
  463.           value: "",
  464.           write: function PU_wrapNode__write(aStr, aLen) {
  465.             this.value += aStr;
  466.           }
  467.         };
  468.         self.serializeNodeAsJSONToOutputStream(convertNode(aNode), writer, true, aForceCopy);
  469.         return writer.value;
  470.       case this.TYPE_X_MOZ_URL:
  471.         function gatherDataUrl(bNode) {
  472.           if (self.nodeIsLivemarkContainer(bNode)) {
  473.             var siteURI = self.livemarks.getSiteURI(bNode.itemId).spec;
  474.             return siteURI + NEWLINE + bNode.title;
  475.           }
  476.           if (self.nodeIsURI(bNode))
  477.             return (aOverrideURI || bNode.uri) + NEWLINE + bNode.title;
  478.           // ignore containers and separators - items without valid URIs
  479.           return "";
  480.         }
  481.         return gatherDataUrl(convertNode(aNode));
  482.  
  483.       case this.TYPE_HTML:
  484.         function gatherDataHtml(bNode) {
  485.           function htmlEscape(s) {
  486.             s = s.replace(/&/g, "&");
  487.             s = s.replace(/>/g, ">");
  488.             s = s.replace(/</g, "<");
  489.             s = s.replace(/"/g, """);
  490.             s = s.replace(/'/g, "'");
  491.             return s;
  492.           }
  493.           // escape out potential HTML in the title
  494.           var escapedTitle = bNode.title ? htmlEscape(bNode.title) : "";
  495.           if (self.nodeIsLivemarkContainer(bNode)) {
  496.             var siteURI = self.livemarks.getSiteURI(bNode.itemId).spec;
  497.             return "<A HREF=\"" + siteURI + "\">" + escapedTitle + "</A>" + NEWLINE;
  498.           }
  499.           if (self.nodeIsContainer(bNode)) {
  500.             asContainer(bNode);
  501.             var wasOpen = bNode.containerOpen;
  502.             if (!wasOpen)
  503.               bNode.containerOpen = true;
  504.  
  505.             var childString = "<DL><DT>" + escapedTitle + "</DT>" + NEWLINE;
  506.             var cc = bNode.childCount;
  507.             for (var i = 0; i < cc; ++i)
  508.               childString += "<DD>"
  509.                              + NEWLINE
  510.                              + gatherDataHtml(bNode.getChild(i))
  511.                              + "</DD>"
  512.                              + NEWLINE;
  513.             bNode.containerOpen = wasOpen;
  514.             return childString + "</DL>" + NEWLINE;
  515.           }
  516.           if (self.nodeIsURI(bNode))
  517.             return "<A HREF=\"" + bNode.uri + "\">" + escapedTitle + "</A>" + NEWLINE;
  518.           if (self.nodeIsSeparator(bNode))
  519.             return "<HR>" + NEWLINE;
  520.           return "";
  521.         }
  522.         return gatherDataHtml(convertNode(aNode));
  523.     }
  524.     // case this.TYPE_UNICODE:
  525.     function gatherDataText(bNode) {
  526.       if (self.nodeIsLivemarkContainer(bNode))
  527.         return self.livemarks.getSiteURI(bNode.itemId).spec;
  528.       if (self.nodeIsContainer(bNode)) {
  529.         asContainer(bNode);
  530.         var wasOpen = bNode.containerOpen;
  531.         if (!wasOpen)
  532.           bNode.containerOpen = true;
  533.  
  534.         var childString = bNode.title + NEWLINE;
  535.         var cc = bNode.childCount;
  536.         for (var i = 0; i < cc; ++i) {
  537.           var child = bNode.getChild(i);
  538.           var suffix = i < (cc - 1) ? NEWLINE : "";
  539.           childString += gatherDataText(child) + suffix;
  540.         }
  541.         bNode.containerOpen = wasOpen;
  542.         return childString;
  543.       }
  544.       if (self.nodeIsURI(bNode))
  545.         return (aOverrideURI || bNode.uri);
  546.       if (self.nodeIsSeparator(bNode))
  547.         return "--------------------";
  548.       return "";
  549.     }
  550.  
  551.     return gatherDataText(convertNode(aNode));
  552.   },
  553.  
  554.   /**
  555.    * Unwraps data from the Clipboard or the current Drag Session.
  556.    * @param   blob
  557.    *          A blob (string) of data, in some format we potentially know how
  558.    *          to parse.
  559.    * @param   type
  560.    *          The content type of the blob.
  561.    * @returns An array of objects representing each item contained by the source.
  562.    */
  563.   unwrapNodes: function PU_unwrapNodes(blob, type) {
  564.     // We split on "\n"  because the transferable system converts "\r\n" to "\n"
  565.     var nodes = [];
  566.     switch(type) {
  567.       case this.TYPE_X_MOZ_PLACE:
  568.       case this.TYPE_X_MOZ_PLACE_SEPARATOR:
  569.       case this.TYPE_X_MOZ_PLACE_CONTAINER:
  570.         var JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
  571.         nodes = JSON.decode("[" + blob + "]");
  572.         break;
  573.       case this.TYPE_X_MOZ_URL:
  574.         var parts = blob.split("\n");
  575.         // data in this type has 2 parts per entry, so if there are fewer
  576.         // than 2 parts left, the blob is malformed and we should stop
  577.         // but drag and drop of files from the shell has parts.length = 1
  578.         if (parts.length != 1 && parts.length % 2)
  579.           break;
  580.         for (var i = 0; i < parts.length; i=i+2) {
  581.           var uriString = parts[i];
  582.           var titleString = "";
  583.           if (parts.length > i+1)
  584.             titleString = parts[i+1];
  585.           else {
  586.             // for drag and drop of files, try to use the leafName as title
  587.             try {
  588.               titleString = this._uri(uriString).QueryInterface(Ci.nsIURL)
  589.                               .fileName;
  590.             }
  591.             catch (e) {}
  592.           }
  593.           // note:  this._uri() will throw if uriString is not a valid URI
  594.           if (this._uri(uriString)) {
  595.             nodes.push({ uri: uriString,
  596.                          title: titleString ? titleString : uriString ,
  597.                          type: this.TYPE_X_MOZ_URL });
  598.           }
  599.         }
  600.         break;
  601.       case this.TYPE_UNICODE:
  602.         var parts = blob.split("\n");
  603.         for (var i = 0; i < parts.length; i++) {
  604.           var uriString = parts[i];
  605.           // note: this._uri() will throw if uriString is not a valid URI
  606.           if (uriString != "" && this._uri(uriString))
  607.             nodes.push({ uri: uriString,
  608.                          title: uriString,
  609.                          type: this.TYPE_X_MOZ_URL });
  610.         }
  611.         break;
  612.       default:
  613.         LOG("Cannot unwrap data of type " + type);
  614.         throw Cr.NS_ERROR_INVALID_ARG;
  615.     }
  616.     return nodes;
  617.   },
  618.  
  619.   /**
  620.    * Generates a nsINavHistoryResult for the contents of a folder.
  621.    * @param   folderId
  622.    *          The folder to open
  623.    * @param   [optional] excludeItems
  624.    *          True to hide all items (individual bookmarks). This is used on
  625.    *          the left places pane so you just get a folder hierarchy.
  626.    * @param   [optional] expandQueries
  627.    *          True to make query items expand as new containers. For managing,
  628.    *          you want this to be false, for menus and such, you want this to
  629.    *          be true.
  630.    * @returns A nsINavHistoryResult containing the contents of the
  631.    *          folder. The result.root is guaranteed to be open.
  632.    */
  633.   getFolderContents:
  634.   function PU_getFolderContents(aFolderId, aExcludeItems, aExpandQueries) {
  635.     var query = this.history.getNewQuery();
  636.     query.setFolders([aFolderId], 1);
  637.     var options = this.history.getNewQueryOptions();
  638.     options.excludeItems = aExcludeItems;
  639.     options.expandQueries = aExpandQueries;
  640.  
  641.     var result = this.history.executeQuery(query, options);
  642.     result.root.containerOpen = true;
  643.     return result;
  644.   },
  645.  
  646.   /**
  647.    * Fetch all annotations for a URI, including all properties of each
  648.    * annotation which would be required to recreate it.
  649.    * @param aURI
  650.    *        The URI for which annotations are to be retrieved.
  651.    * @return Array of objects, each containing the following properties:
  652.    *         name, flags, expires, mimeType, type, value
  653.    */
  654.   getAnnotationsForURI: function PU_getAnnotationsForURI(aURI) {
  655.     var annosvc = this.annotations;
  656.     var annos = [], val = null;
  657.     var annoNames = annosvc.getPageAnnotationNames(aURI, {});
  658.     for (var i = 0; i < annoNames.length; i++) {
  659.       var flags = {}, exp = {}, mimeType = {}, storageType = {};
  660.       annosvc.getPageAnnotationInfo(aURI, annoNames[i], flags, exp, mimeType, storageType);
  661.       if (storageType.value == annosvc.TYPE_BINARY) {
  662.         var data = {}, length = {}, mimeType = {};
  663.         annosvc.getPageAnnotationBinary(aURI, annoNames[i], data, length, mimeType);
  664.         val = data.value;
  665.       }
  666.       else
  667.         val = annosvc.getPageAnnotation(aURI, annoNames[i]);
  668.  
  669.       annos.push({name: annoNames[i],
  670.                   flags: flags.value,
  671.                   expires: exp.value,
  672.                   mimeType: mimeType.value,
  673.                   type: storageType.value,
  674.                   value: val});
  675.     }
  676.     return annos;
  677.   },
  678.  
  679.   /**
  680.    * Fetch all annotations for an item, including all properties of each
  681.    * annotation which would be required to recreate it.
  682.    * @param aItemId
  683.    *        The identifier of the itme for which annotations are to be
  684.    *        retrieved.
  685.    * @return Array of objects, each containing the following properties:
  686.    *         name, flags, expires, mimeType, type, value
  687.    */
  688.   getAnnotationsForItem: function PU_getAnnotationsForItem(aItemId) {
  689.     var annosvc = this.annotations;
  690.     var annos = [], val = null;
  691.     var annoNames = annosvc.getItemAnnotationNames(aItemId, {});
  692.     for (var i = 0; i < annoNames.length; i++) {
  693.       var flags = {}, exp = {}, mimeType = {}, storageType = {};
  694.       annosvc.getItemAnnotationInfo(aItemId, annoNames[i], flags, exp, mimeType, storageType);
  695.       if (storageType.value == annosvc.TYPE_BINARY) {
  696.         var data = {}, length = {}, mimeType = {};
  697.         annosvc.geItemAnnotationBinary(aItemId, annoNames[i], data, length, mimeType);
  698.         val = data.value;
  699.       }
  700.       else
  701.         val = annosvc.getItemAnnotation(aItemId, annoNames[i]);
  702.  
  703.       annos.push({name: annoNames[i],
  704.                   flags: flags.value,
  705.                   expires: exp.value,
  706.                   mimeType: mimeType.value,
  707.                   type: storageType.value,
  708.                   value: val});
  709.     }
  710.     return annos;
  711.   },
  712.  
  713.   /**
  714.    * Annotate a URI with a batch of annotations.
  715.    * @param aURI
  716.    *        The URI for which annotations are to be set.
  717.    * @param aAnnotations
  718.    *        Array of objects, each containing the following properties:
  719.    *        name, flags, expires, type, mimeType (only used for binary
  720.    *        annotations) value.
  721.    */
  722.   setAnnotationsForURI: function PU_setAnnotationsForURI(aURI, aAnnos) {
  723.     var annosvc = this.annotations;
  724.     aAnnos.forEach(function(anno) {
  725.       var flags = ("flags" in anno) ? anno.flags : 0;
  726.       var expires = ("expires" in anno) ?
  727.         anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
  728.       if (anno.type == annosvc.TYPE_BINARY) {
  729.         annosvc.setPageAnnotationBinary(aURI, anno.name, anno.value,
  730.                                         anno.value.length, anno.mimeType,
  731.                                         flags, expires);
  732.       }
  733.       else
  734.         annosvc.setPageAnnotation(aURI, anno.name, anno.value, flags, expires);
  735.     });
  736.   },
  737.  
  738.   /**
  739.    * Annotate an item with a batch of annotations.
  740.    * @param aItemId
  741.    *        The identifier of the item for which annotations are to be set
  742.    * @param aAnnotations
  743.    *        Array of objects, each containing the following properties:
  744.    *        name, flags, expires, type, mimeType (only used for binary
  745.    *        annotations) value.
  746.    */
  747.   setAnnotationsForItem: function PU_setAnnotationsForItem(aItemId, aAnnos) {
  748.     var annosvc = this.annotations;
  749.     aAnnos.forEach(function(anno) {
  750.       var flags = ("flags" in anno) ? anno.flags : 0;
  751.       var expires = ("expires" in anno) ?
  752.         anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
  753.       if (anno.type == annosvc.TYPE_BINARY) {
  754.         annosvc.setItemAnnotationBinary(aItemId, anno.name, anno.value,
  755.                                         anno.value.length, anno.mimeType,
  756.                                         flags, expires);
  757.       }
  758.       else {
  759.         annosvc.setItemAnnotation(aItemId, anno.name, anno.value, flags,
  760.                                   expires);
  761.       }
  762.     });
  763.   },
  764.  
  765.   /**
  766.    * Helper for getting a serialized Places query for a particular folder.
  767.    * @param aFolderId The folder id to get a query for.
  768.    * @return string serialized place URI
  769.    */
  770.   getQueryStringForFolder: function PU_getQueryStringForFolder(aFolderId) {
  771.     var options = this.history.getNewQueryOptions();
  772.     var query = this.history.getNewQuery();
  773.     query.setFolders([aFolderId], 1);
  774.     return this.history.queriesToQueryString([query], 1, options);
  775.   },
  776.  
  777.   // identifier getters for special folders
  778.   get placesRootId() {
  779.     delete this.placesRootId;
  780.     return this.placesRootId = this.bookmarks.placesRoot;
  781.   },
  782.  
  783.   get bookmarksMenuFolderId() {
  784.     delete this.bookmarksMenuFolderId;
  785.     return this.bookmarksMenuFolderId = this.bookmarks.bookmarksMenuFolder;
  786.   },
  787.  
  788.   get toolbarFolderId() {
  789.     delete this.toolbarFolderId;
  790.     return this.toolbarFolderId = this.bookmarks.toolbarFolder;
  791.   },
  792.  
  793.   get tagsFolderId() {
  794.     delete this.tagsFolderId;
  795.     return this.tagsFolderId = this.bookmarks.tagsFolder;
  796.   },
  797.  
  798.   get unfiledBookmarksFolderId() {
  799.     delete this.unfiledBookmarksFolderId;
  800.     return this.unfiledBookmarksFolderId = this.bookmarks.unfiledBookmarksFolder;
  801.   },
  802.  
  803.   /**
  804.    * Set the POST data associated with a bookmark, if any.
  805.    * Used by POST keywords.
  806.    *   @param aBookmarkId
  807.    *   @returns string of POST data
  808.    */
  809.   setPostDataForBookmark: function PU_setPostDataForBookmark(aBookmarkId, aPostData) {
  810.     const annos = this.annotations;
  811.     if (aPostData)
  812.       annos.setItemAnnotation(aBookmarkId, POST_DATA_ANNO, aPostData, 
  813.                               0, Ci.nsIAnnotationService.EXPIRE_NEVER);
  814.     else if (annos.itemHasAnnotation(aBookmarkId, POST_DATA_ANNO))
  815.       annos.removeItemAnnotation(aBookmarkId, POST_DATA_ANNO);
  816.   },
  817.  
  818.   /**
  819.    * Get the POST data associated with a bookmark, if any.
  820.    * @param aBookmarkId
  821.    * @returns string of POST data if set for aBookmarkId. null otherwise.
  822.    */
  823.   getPostDataForBookmark: function PU_getPostDataForBookmark(aBookmarkId) {
  824.     const annos = this.annotations;
  825.     if (annos.itemHasAnnotation(aBookmarkId, POST_DATA_ANNO))
  826.       return annos.getItemAnnotation(aBookmarkId, POST_DATA_ANNO);
  827.  
  828.     return null;
  829.   },
  830.  
  831.   /**
  832.    * Get the URI (and any associated POST data) for a given keyword.
  833.    * @param aKeyword string keyword
  834.    * @returns an array containing a string URL and a string of POST data
  835.    */
  836.   getURLAndPostDataForKeyword: function PU_getURLAndPostDataForKeyword(aKeyword) {
  837.     var url = null, postdata = null;
  838.     try {
  839.       var uri = this.bookmarks.getURIForKeyword(aKeyword);
  840.       if (uri) {
  841.         url = uri.spec;
  842.         var bookmarks = this.bookmarks.getBookmarkIdsForURI(uri, {});
  843.         for (let i = 0; i < bookmarks.length; i++) {
  844.           var bookmark = bookmarks[i];
  845.           var kw = this.bookmarks.getKeywordForBookmark(bookmark);
  846.           if (kw == aKeyword) {
  847.             postdata = this.getPostDataForBookmark(bookmark);
  848.             break;
  849.           }
  850.         }
  851.       }
  852.     } catch(ex) {}
  853.     return [url, postdata];
  854.   },
  855.  
  856.   /**
  857.    * Get all bookmarks for a URL, excluding items under tag or livemark
  858.    * containers.
  859.    */
  860.   getBookmarksForURI:
  861.   function PU_getBookmarksForURI(aURI) {
  862.     var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI, {});
  863.  
  864.     // filter the ids list
  865.     return bmkIds.filter(function(aID) {
  866.       var parent = this.bookmarks.getFolderIdForItem(aID);
  867.       // Livemark child
  868.       if (this.annotations.itemHasAnnotation(parent, LMANNO_FEEDURI))
  869.         return false;
  870.       var grandparent = this.bookmarks.getFolderIdForItem(parent);
  871.       // item under a tag container
  872.       if (grandparent == this.tagsFolderId)
  873.         return false;
  874.       return true;
  875.     }, this);
  876.   },
  877.  
  878.   /**
  879.    * Get the most recently added/modified bookmark for a URL, excluding items
  880.    * under tag or livemark containers. -1 is returned if no item is found.
  881.    */
  882.   getMostRecentBookmarkForURI:
  883.   function PU_getMostRecentBookmarkForURI(aURI) {
  884.     var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI, {});
  885.     for (var i = 0; i < bmkIds.length; i++) {
  886.       // Find the first folder which isn't a tag container
  887.       var bk = bmkIds[i];
  888.       var parent = this.bookmarks.getFolderIdForItem(bk);
  889.       if (parent == this.unfiledBookmarksFolderId)
  890.         return bk;
  891.  
  892.       var grandparent = this.bookmarks.getFolderIdForItem(parent);
  893.       if (grandparent != this.tagsFolderId &&
  894.           !this.annotations.itemHasAnnotation(parent, LMANNO_FEEDURI))
  895.         return bk;
  896.     }
  897.     return -1;
  898.   },
  899.  
  900.   getMostRecentFolderForFeedURI:
  901.   function PU_getMostRecentFolderForFeedURI(aURI) {
  902.     var feedSpec = aURI.spec
  903.     var annosvc = this.annotations;
  904.     var livemarks = annosvc.getItemsWithAnnotation(LMANNO_FEEDURI, {});
  905.     for (var i = 0; i < livemarks.length; i++) {
  906.       if (annosvc.getItemAnnotation(livemarks[i], LMANNO_FEEDURI) == feedSpec)
  907.         return livemarks[i];
  908.     }
  909.     return -1;
  910.   },
  911.  
  912.   // Returns true if a container has uris in its first level
  913.   // Has better performances than checking getURLsForContainerNode(node).length
  914.   hasChildURIs: function PU_hasChildURIs(aNode) {
  915.     if (!this.nodeIsContainer(aNode))
  916.       return false;
  917.  
  918.     // in the Library left pane we use excludeItems
  919.     if (this.nodeIsFolder(aNode) && asQuery(aNode).queryOptions.excludeItems) {
  920.       var itemId = PlacesUtils.getConcreteItemId(aNode);
  921.       var contents = this.getFolderContents(itemId, false, false).root;
  922.       for (var i = 0; i < contents.childCount; ++i) {
  923.         var child = contents.getChild(i);
  924.         if (this.nodeIsURI(child))
  925.           return true;
  926.       }
  927.       return false;
  928.     }
  929.  
  930.     var wasOpen = aNode.containerOpen;
  931.     if (!wasOpen)
  932.       aNode.containerOpen = true;
  933.     var found = false;
  934.     for (var i = 0; i < aNode.childCount && !found; i++) {
  935.       var child = aNode.getChild(i);
  936.       if (this.nodeIsURI(child))
  937.         found = true;
  938.     }
  939.     if (!wasOpen)
  940.       aNode.containerOpen = false;
  941.     return found;
  942.   },
  943.  
  944.   getURLsForContainerNode: function PU_getURLsForContainerNode(aNode) {
  945.     let urls = [];
  946.     if (this.nodeIsFolder(aNode) && asQuery(aNode).queryOptions.excludeItems) {
  947.       // grab manually
  948.       var itemId = this.getConcreteItemId(aNode);
  949.       let contents = this.getFolderContents(itemId, false, false).root;
  950.       for (let i = 0; i < contents.childCount; ++i) {
  951.         let child = contents.getChild(i);
  952.         if (this.nodeIsURI(child))
  953.           urls.push({uri: child.uri, isBookmark: this.nodeIsBookmark(child)});
  954.       }
  955.     }
  956.     else {
  957.       let result, oldViewer, wasOpen;
  958.       try {
  959.         let wasOpen = aNode.containerOpen;
  960.         result = aNode.parentResult;
  961.         oldViewer = result.viewer;
  962.         if (!wasOpen) {
  963.           result.viewer = null;
  964.           aNode.containerOpen = true;
  965.         }
  966.         for (let i = 0; i < aNode.childCount; ++i) {
  967.           // Include visible url nodes only
  968.           let child = aNode.getChild(i);
  969.           if (this.nodeIsURI(child)) {
  970.             // If the node contents is visible, add the uri only if its node is
  971.             // visible. Otherwise follow viewer's collapseDuplicates property,
  972.             // default to true
  973.             if ((wasOpen && oldViewer && child.viewIndex != -1) ||
  974.                 (oldViewer && !oldViewer.collapseDuplicates) ||
  975.                 urls.indexOf(child.uri) == -1) {
  976.               urls.push({ uri: child.uri,
  977.                           isBookmark: this.nodeIsBookmark(child) });
  978.             }
  979.           }
  980.         }
  981.         if (!wasOpen)
  982.           aNode.containerOpen = false;
  983.       }
  984.       finally {
  985.         if (!wasOpen)
  986.           result.viewer = oldViewer;
  987.       }
  988.     }
  989.  
  990.     return urls;
  991.   },
  992.  
  993.   /**
  994.    * Restores bookmarks/tags from a JSON file.
  995.    * WARNING: This method *removes* any bookmarks in the collection before
  996.    * restoring from the file.
  997.    *
  998.    * @param aFile
  999.    *        nsIFile of bookmarks in JSON format to be restored.
  1000.    * @param aExcludeItems
  1001.    *        Array of root item ids (ie: children of the places root)
  1002.    *        to not delete when restoring.
  1003.    */
  1004.   restoreBookmarksFromJSONFile:
  1005.   function PU_restoreBookmarksFromJSONFile(aFile, aExcludeItems) {
  1006.     // open file stream
  1007.     var stream = Cc["@mozilla.org/network/file-input-stream;1"].
  1008.                  createInstance(Ci.nsIFileInputStream);
  1009.     stream.init(aFile, 0x01, 0, 0);
  1010.     var converted = Cc["@mozilla.org/intl/converter-input-stream;1"].
  1011.                     createInstance(Ci.nsIConverterInputStream);
  1012.     converted.init(stream, "UTF-8", 1024,
  1013.                    Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
  1014.  
  1015.     // read in contents
  1016.     var str = {};
  1017.     var jsonStr = "";
  1018.     while (converted.readString(4096, str) != 0)
  1019.       jsonStr += str.value;
  1020.     converted.close();
  1021.  
  1022.     if (jsonStr.length == 0)
  1023.       return; // empty file
  1024.  
  1025.     this.restoreBookmarksFromJSONString(jsonStr, true, aExcludeItems);
  1026.   },
  1027.  
  1028.   /**
  1029.    * Import bookmarks from a JSON string.
  1030.    * 
  1031.    * @param aString
  1032.    *        JSON string of serialized bookmark data.
  1033.    * @param aReplace
  1034.    *        Boolean if true, replace existing bookmarks, else merge.
  1035.    * @param aExcludeItems
  1036.    *        Array of root item ids (ie: children of the places root)
  1037.    *        to not delete when restoring.
  1038.    */
  1039.   restoreBookmarksFromJSONString:
  1040.   function PU_restoreBookmarksFromJSONString(aString, aReplace, aExcludeItems) {
  1041.     // convert string to JSON
  1042.     var nodes = this.unwrapNodes(aString, this.TYPE_X_MOZ_PLACE_CONTAINER);
  1043.  
  1044.     if (nodes.length == 0 || !nodes[0].children ||
  1045.         nodes[0].children.length == 0)
  1046.       return; // nothing to restore
  1047.  
  1048.     // ensure tag folder gets processed last
  1049.     nodes[0].children.sort(function sortRoots(aNode, bNode) {
  1050.       return (aNode.root && aNode.root == "tagsFolder") ? 1 :
  1051.               (bNode.root && bNode.root == "tagsFolder") ? -1 : 0;
  1052.     });
  1053.  
  1054.     var batch = {
  1055.       _utils: this,
  1056.       nodes: nodes[0].children,
  1057.       runBatched: function restore_runBatched() {
  1058.         if (aReplace) {
  1059.           var excludeItems = aExcludeItems || [];
  1060.           // delete existing children of the root node, excepting:
  1061.           // 1. special folders: delete the child nodes 
  1062.           // 2. tags folder: untag via the tagging api
  1063.           var query = this._utils.history.getNewQuery();
  1064.           query.setFolders([this._utils.placesRootId], 1);
  1065.           var options = this._utils.history.getNewQueryOptions();
  1066.           options.expandQueries = false;
  1067.           var root = this._utils.history.executeQuery(query, options).root;
  1068.           root.containerOpen = true;
  1069.           var childIds = [];
  1070.           for (var i = 0; i < root.childCount; i++) {
  1071.             var childId = root.getChild(i).itemId;
  1072.             if (excludeItems.indexOf(childId) == -1)
  1073.               childIds.push(childId);
  1074.           }
  1075.           root.containerOpen = false;
  1076.  
  1077.           for (var i = 0; i < childIds.length; i++) {
  1078.             var rootItemId = childIds[i];
  1079.             if (rootItemId == this._utils.tagsFolderId) {
  1080.               // remove tags via the tagging service
  1081.               var tags = this._utils.tagging.allTags;
  1082.               var uris = [];
  1083.               var bogusTagContainer = false;
  1084.               for (let i in tags) {
  1085.                 var tagURIs = [];
  1086.                 // skip empty tags since getURIsForTag would throw
  1087.                 if (tags[i])
  1088.                   tagURIs = this._utils.tagging.getURIsForTag(tags[i]);
  1089.  
  1090.                 if (!tagURIs.length) {
  1091.                   // This is a bogus tag container, empty tags should be removed
  1092.                   // automatically, but this does not work if they contain some
  1093.                   // not-uri node, so we remove them manually.
  1094.                   // XXX this is a temporary workaround until we implement
  1095.                   // preventive database maintainance in bug 431558.
  1096.                   bogusTagContainer = true;
  1097.                 }
  1098.                 for (let j in tagURIs)
  1099.                   this._utils.tagging.untagURI(tagURIs[j], [tags[i]]);
  1100.               }
  1101.               if (bogusTagContainer)
  1102.                 this._utils.bookmarks.removeFolderChildren(rootItemId);
  1103.             }
  1104.             else if ([this._utils.toolbarFolderId,
  1105.                       this._utils.unfiledBookmarksFolderId,
  1106.                       this._utils.bookmarksMenuFolderId].indexOf(rootItemId) != -1)
  1107.               this._utils.bookmarks.removeFolderChildren(rootItemId);
  1108.             else
  1109.               this._utils.bookmarks.removeItem(rootItemId);
  1110.           }
  1111.         }
  1112.  
  1113.         var searchIds = [];
  1114.         var folderIdMap = [];
  1115.  
  1116.         this.nodes.forEach(function(node) {
  1117.           if (!node.children || node.children.length == 0)
  1118.             return; // nothing to restore for this root
  1119.  
  1120.           if (node.root) {
  1121.             var container = this.placesRootId; // default to places root
  1122.             switch (node.root) {
  1123.               case "bookmarksMenuFolder":
  1124.                 container = this.bookmarksMenuFolderId;
  1125.                 break;
  1126.               case "tagsFolder":
  1127.                 container = this.tagsFolderId;
  1128.                 break;
  1129.               case "unfiledBookmarksFolder":
  1130.                 container = this.unfiledBookmarksFolderId;
  1131.                 break;
  1132.               case "toolbarFolder":
  1133.                 container = this.toolbarFolderId;
  1134.                 break;
  1135.             }
  1136.  
  1137.             // insert the data into the db
  1138.             node.children.forEach(function(child) {
  1139.               var index = child.index;
  1140.               var [folders, searches] = this.importJSONNode(child, container, index);
  1141.               folderIdMap = folderIdMap.concat(folders);
  1142.               searchIds = searchIds.concat(searches);
  1143.             }, this);
  1144.           }
  1145.           else
  1146.             this.importJSONNode(node, this.placesRootId, node.index);
  1147.  
  1148.         }, this._utils);
  1149.  
  1150.         // fixup imported place: uris that contain folders
  1151.         searchIds.forEach(function(aId) {
  1152.           var oldURI = this.bookmarks.getBookmarkURI(aId);
  1153.           var uri = this._fixupQuery(this.bookmarks.getBookmarkURI(aId),
  1154.                                      folderIdMap);
  1155.           if (!uri.equals(oldURI))
  1156.             this.bookmarks.changeBookmarkURI(aId, uri);
  1157.         }, this._utils);
  1158.       }
  1159.     };
  1160.  
  1161.     this.bookmarks.runInBatchMode(batch, null);
  1162.   },
  1163.  
  1164.   /**
  1165.    * Takes a JSON-serialized node and inserts it into the db.
  1166.    *
  1167.    * @param   aData
  1168.    *          The unwrapped data blob of dropped or pasted data.
  1169.    * @param   aContainer
  1170.    *          The container the data was dropped or pasted into
  1171.    * @param   aIndex
  1172.    *          The index within the container the item was dropped or pasted at
  1173.    * @returns an array containing of maps of old folder ids to new folder ids,
  1174.    *          and an array of saved search ids that need to be fixed up.
  1175.    *          eg: [[[oldFolder1, newFolder1]], [search1]]
  1176.    */
  1177.   importJSONNode: function PU_importJSONNode(aData, aContainer, aIndex) {
  1178.     var folderIdMap = [];
  1179.     var searchIds = [];
  1180.     var id = -1;
  1181.     switch (aData.type) {
  1182.       case this.TYPE_X_MOZ_PLACE_CONTAINER:
  1183.         if (aContainer == PlacesUtils.bookmarks.tagsFolder) {
  1184.           if (aData.children) {
  1185.             aData.children.forEach(function(aChild) {
  1186.               try {
  1187.                 this.tagging.tagURI(this._uri(aChild.uri), [aData.title]);
  1188.               } catch (ex) {
  1189.                 // invalid tag child, skip it
  1190.               }
  1191.             }, this);
  1192.             return [folderIdMap, searchIds];
  1193.           }
  1194.         }
  1195.         else if (aData.livemark && aData.annos) {
  1196.           // node is a livemark
  1197.           var feedURI = null;
  1198.           var siteURI = null;
  1199.           aData.annos = aData.annos.filter(function(aAnno) {
  1200.             if (aAnno.name == LMANNO_FEEDURI) {
  1201.               feedURI = this._uri(aAnno.value);
  1202.               return false;
  1203.             }
  1204.             else if (aAnno.name == LMANNO_SITEURI) {
  1205.               siteURI = this._uri(aAnno.value);
  1206.               return false;
  1207.             }
  1208.             return true;
  1209.           }, this);
  1210.  
  1211.           if (feedURI)
  1212.             id = this.livemarks.createLivemark(aContainer, aData.title, siteURI, feedURI, aIndex);
  1213.         }
  1214.         else {
  1215.           id = this.bookmarks.createFolder(aContainer, aData.title, aIndex);
  1216.           folderIdMap.push([aData.id, id]);
  1217.           // process children
  1218.           if (aData.children) {
  1219.             aData.children.every(function(aChild, aIndex) {
  1220.               var [folderIds, searches] = this.importJSONNode(aChild, id, aIndex);
  1221.               folderIdMap = folderIdMap.concat(folderIds);
  1222.               searchIds = searchIds.concat(searches);
  1223.               return true;
  1224.             }, this);
  1225.           }
  1226.         }
  1227.         break;
  1228.       case this.TYPE_X_MOZ_PLACE:
  1229.         id = this.bookmarks.insertBookmark(aContainer, this._uri(aData.uri), aIndex, aData.title);
  1230.         if (aData.keyword)
  1231.           this.bookmarks.setKeywordForBookmark(id, aData.keyword);
  1232.         if (aData.tags) {
  1233.           var tags = aData.tags.split(", ");
  1234.           if (tags.length)
  1235.             this.tagging.tagURI(this._uri(aData.uri), tags);
  1236.         }
  1237.         if (aData.charset)
  1238.           this.history.setCharsetForURI(this._uri(aData.uri), aData.charset);
  1239.         if (aData.uri.match(/^place:/))
  1240.           searchIds.push(id);
  1241.         break;
  1242.       case this.TYPE_X_MOZ_PLACE_SEPARATOR:
  1243.         id = this.bookmarks.insertSeparator(aContainer, aIndex);
  1244.         break;
  1245.       default:
  1246.     }
  1247.  
  1248.     // set generic properties
  1249.     if (id != -1) {
  1250.       this.bookmarks.setItemDateAdded(id, aData.dateAdded);
  1251.       this.bookmarks.setItemLastModified(id, aData.lastModified);
  1252.       if (aData.annos)
  1253.         this.setAnnotationsForItem(id, aData.annos);
  1254.     }
  1255.  
  1256.     return [folderIdMap, searchIds];
  1257.   },
  1258.  
  1259.   /**
  1260.    * Replaces imported folder ids with their local counterparts in a place: URI.
  1261.    *
  1262.    * @param   aURI
  1263.    *          A place: URI with folder ids.
  1264.    * @param   aFolderIdMap
  1265.    *          An array mapping old folder id to new folder ids.
  1266.    * @returns the fixed up URI if all matched. If some matched, it returns
  1267.    *          the URI with only the matching folders included. If none matched it
  1268.    *          returns the input URI unchanged.
  1269.    */
  1270.   _fixupQuery: function PU__fixupQuery(aQueryURI, aFolderIdMap) {
  1271.     var queries = {};
  1272.     var options = {};
  1273.     this.history.queryStringToQueries(aQueryURI.spec, queries, {}, options);
  1274.  
  1275.     var fixedQueries = [];
  1276.     queries.value.forEach(function(aQuery) {
  1277.       var folders = aQuery.getFolders({});
  1278.  
  1279.       var newFolders = [];
  1280.       aFolderIdMap.forEach(function(aMapping) {
  1281.         if (folders.indexOf(aMapping[0]) != -1)
  1282.           newFolders.push(aMapping[1]);
  1283.       });
  1284.  
  1285.       if (newFolders.length)
  1286.         aQuery.setFolders(newFolders, newFolders.length);
  1287.       fixedQueries.push(aQuery);
  1288.     });
  1289.  
  1290.     var stringURI = this.history.queriesToQueryString(fixedQueries,
  1291.                                                       fixedQueries.length,
  1292.                                                       options.value);
  1293.     return this._uri(stringURI);
  1294.   },
  1295.  
  1296.   /**
  1297.    * Serializes the given node (and all it's descendents) as JSON
  1298.    * and writes the serialization to the given output stream.
  1299.    * 
  1300.    * @param   aNode
  1301.    *          An nsINavHistoryResultNode
  1302.    * @param   aStream
  1303.    *          An nsIOutputStream. NOTE: it only uses the write(str, len)
  1304.    *          method of nsIOutputStream. The caller is responsible for
  1305.    *          closing the stream.
  1306.    * @param   aIsUICommand
  1307.    *          Boolean - If true, modifies serialization so that each node self-contained.
  1308.    *          For Example, tags are serialized inline with each bookmark.
  1309.    * @param   aResolveShortcuts
  1310.    *          Converts folder shortcuts into actual folders. 
  1311.    * @param   aExcludeItems
  1312.    *          An array of item ids that should not be written to the backup.
  1313.    */
  1314.   serializeNodeAsJSONToOutputStream:
  1315.   function PU_serializeNodeAsJSONToOutputStream(aNode, aStream, aIsUICommand,
  1316.                                                 aResolveShortcuts,
  1317.                                                 aExcludeItems) {
  1318.     var self = this;
  1319.     
  1320.     function addGenericProperties(aPlacesNode, aJSNode) {
  1321.       aJSNode.title = aPlacesNode.title;
  1322.       var id = aPlacesNode.itemId;
  1323.       if (id != -1) {
  1324.         aJSNode.id = id;
  1325.  
  1326.         var parent = aPlacesNode.parent;
  1327.         if (parent)
  1328.           aJSNode.parent = parent.itemId;
  1329.         var dateAdded = aPlacesNode.dateAdded;
  1330.         if (dateAdded)
  1331.           aJSNode.dateAdded = dateAdded;
  1332.         var lastModified = aPlacesNode.lastModified;
  1333.         if (lastModified)
  1334.           aJSNode.lastModified = lastModified;
  1335.  
  1336.         // XXX need a hasAnnos api
  1337.         var annos = [];
  1338.         try {
  1339.           annos = self.getAnnotationsForItem(id).filter(function(anno) {
  1340.             // XXX should whitelist this instead, w/ a pref for
  1341.             // backup/restore of non-whitelisted annos
  1342.             // XXX causes JSON encoding errors, so utf-8 encode
  1343.             //anno.value = unescape(encodeURIComponent(anno.value));
  1344.             if (anno.name == LMANNO_FEEDURI)
  1345.               aJSNode.livemark = 1;
  1346.             if (anno.name == READ_ONLY_ANNO && aResolveShortcuts) {
  1347.               // When copying a read-only node, remove the read-only annotation.
  1348.               return false;
  1349.             }
  1350.             return true;
  1351.           });
  1352.         } catch(ex) {
  1353.           LOG(ex);
  1354.         }
  1355.         if (annos.length != 0)
  1356.           aJSNode.annos = annos;
  1357.       }
  1358.       // XXXdietrich - store annos for non-bookmark items
  1359.     }
  1360.  
  1361.     function addURIProperties(aPlacesNode, aJSNode) {
  1362.       aJSNode.type = self.TYPE_X_MOZ_PLACE;
  1363.       aJSNode.uri = aPlacesNode.uri;
  1364.       if (aJSNode.id && aJSNode.id != -1) {
  1365.         // harvest bookmark-specific properties
  1366.         var keyword = self.bookmarks.getKeywordForBookmark(aJSNode.id);
  1367.         if (keyword)
  1368.           aJSNode.keyword = keyword;
  1369.       }
  1370.  
  1371.       var tags = aIsUICommand ? aPlacesNode.tags : null;
  1372.       if (tags)
  1373.         aJSNode.tags = tags;
  1374.  
  1375.       // last character-set
  1376.       var uri = self._uri(aPlacesNode.uri);
  1377.       var lastCharset = self.history.getCharsetForURI(uri);
  1378.       if (lastCharset)
  1379.         aJSNode.charset = lastCharset;
  1380.     }
  1381.  
  1382.     function addSeparatorProperties(aPlacesNode, aJSNode) {
  1383.       aJSNode.type = self.TYPE_X_MOZ_PLACE_SEPARATOR;
  1384.     }
  1385.  
  1386.     function addContainerProperties(aPlacesNode, aJSNode) {
  1387.       // saved queries
  1388.       var concreteId = PlacesUtils.getConcreteItemId(aPlacesNode);
  1389.       if (aJSNode.id != -1 && (PlacesUtils.nodeIsQuery(aPlacesNode) ||
  1390.           (concreteId != aPlacesNode.itemId && !aResolveShortcuts))) {
  1391.         aJSNode.type = self.TYPE_X_MOZ_PLACE;
  1392.         aJSNode.uri = aPlacesNode.uri;
  1393.         // folder shortcut
  1394.         if (aIsUICommand)
  1395.           aJSNode.concreteId = concreteId;
  1396.         return;
  1397.       }
  1398.       else if (aJSNode.id != -1) { // bookmark folder
  1399.         if (concreteId != aPlacesNode.itemId)
  1400.         aJSNode.type = self.TYPE_X_MOZ_PLACE;
  1401.         aJSNode.type = self.TYPE_X_MOZ_PLACE_CONTAINER;
  1402.         // mark special folders
  1403.         if (aJSNode.id == self.bookmarks.placesRoot)
  1404.           aJSNode.root = "placesRoot";
  1405.         else if (aJSNode.id == self.bookmarks.bookmarksMenuFolder)
  1406.           aJSNode.root = "bookmarksMenuFolder";
  1407.         else if (aJSNode.id == self.bookmarks.tagsFolder)
  1408.           aJSNode.root = "tagsFolder";
  1409.         else if (aJSNode.id == self.bookmarks.unfiledBookmarksFolder)
  1410.           aJSNode.root = "unfiledBookmarksFolder";
  1411.         else if (aJSNode.id == self.bookmarks.toolbarFolder)
  1412.           aJSNode.root = "toolbarFolder";
  1413.       }
  1414.     }
  1415.  
  1416.     function writeScalarNode(aStream, aNode) {
  1417.       // serialize to json
  1418.       var jstr = self.toJSONString(aNode);
  1419.       // write to stream
  1420.       aStream.write(jstr, jstr.length);
  1421.     }
  1422.  
  1423.     function writeComplexNode(aStream, aNode, aSourceNode) {
  1424.       var escJSONStringRegExp = /(["\\])/g;
  1425.       // write prefix
  1426.       var properties = [];
  1427.       for (let [name, value] in Iterator(aNode)) {
  1428.         if (name == "annos")
  1429.           value = self.toJSONString(value);
  1430.         else if (typeof value == "string")
  1431.           value = "\"" + value.replace(escJSONStringRegExp, '\\$1') + "\"";
  1432.         properties.push("\"" + name.replace(escJSONStringRegExp, '\\$1') + "\":" + value);
  1433.       }
  1434.       var jStr = "{" + properties.join(",") + ",\"children\":[";
  1435.       aStream.write(jStr, jStr.length);
  1436.  
  1437.       // write child nodes
  1438.       if (!aNode.livemark) {
  1439.         asContainer(aSourceNode);
  1440.         var wasOpen = aSourceNode.containerOpen;
  1441.         if (!wasOpen)
  1442.           aSourceNode.containerOpen = true;
  1443.         var cc = aSourceNode.childCount;
  1444.         for (var i = 0; i < cc; ++i) {
  1445.           var childNode = aSourceNode.getChild(i);
  1446.           if (aExcludeItems && aExcludeItems.indexOf(childNode.itemId) != -1)
  1447.             continue;
  1448.           var written = serializeNodeToJSONStream(aSourceNode.getChild(i), i);
  1449.           if (written && i < cc - 1)
  1450.             aStream.write(",", 1);
  1451.         }
  1452.         if (!wasOpen)
  1453.           aSourceNode.containerOpen = false;
  1454.       }
  1455.  
  1456.       // write suffix
  1457.       aStream.write("]}", 2);
  1458.     }
  1459.  
  1460.     function serializeNodeToJSONStream(bNode, aIndex) {
  1461.       var node = {};
  1462.  
  1463.       // set index in order received
  1464.       // XXX handy shortcut, but are there cases where we don't want
  1465.       // to export using the sorting provided by the query?
  1466.       if (aIndex)
  1467.         node.index = aIndex;
  1468.  
  1469.       addGenericProperties(bNode, node);
  1470.  
  1471.       var parent = bNode.parent;
  1472.       var grandParent = parent ? parent.parent : null;
  1473.  
  1474.       if (self.nodeIsURI(bNode)) {
  1475.         // Tag root accept only folder nodes
  1476.         if (parent && parent.itemId == self.tagsFolderId)
  1477.           return false;
  1478.         // Check for url validity, since we can't halt while writing a backup.
  1479.         // This will throw if we try to serialize an invalid url and it does
  1480.         // not make sense saving a wrong or corrupt uri node.
  1481.         try {
  1482.           self._uri(bNode.uri);
  1483.         } catch (ex) {
  1484.           return false;
  1485.         }
  1486.         addURIProperties(bNode, node);
  1487.       }
  1488.       else if (self.nodeIsContainer(bNode)) {
  1489.         // Tag containers accept only uri nodes
  1490.         if (grandParent && grandParent.itemId == self.tagsFolderId)
  1491.           return false;
  1492.         addContainerProperties(bNode, node);
  1493.       }
  1494.       else if (self.nodeIsSeparator(bNode)) {
  1495.         // Tag root accept only folder nodes
  1496.         // Tag containers accept only uri nodes
  1497.         if ((parent && parent.itemId == self.tagsFolderId) ||
  1498.             (grandParent && grandParent.itemId == self.tagsFolderId))
  1499.           return false;
  1500.  
  1501.         addSeparatorProperties(bNode, node);
  1502.       }
  1503.  
  1504.       if (!node.feedURI && node.type == self.TYPE_X_MOZ_PLACE_CONTAINER)
  1505.         writeComplexNode(aStream, node, bNode);
  1506.       else
  1507.         writeScalarNode(aStream, node);
  1508.       return true;
  1509.     }
  1510.  
  1511.     // serialize to stream
  1512.     serializeNodeToJSONStream(aNode, null);
  1513.   },
  1514.  
  1515.   /**
  1516.    * Serialize a JS object to JSON
  1517.    */
  1518.   toJSONString: function PU_toJSONString(aObj) {
  1519.     var JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
  1520.     return JSON.encode(aObj);
  1521.   },
  1522.  
  1523.   /**
  1524.    * Serializes bookmarks using JSON, and writes to the supplied file.
  1525.    */
  1526.   backupBookmarksToFile: function PU_backupBookmarksToFile(aFile, aExcludeItems) {
  1527.     if (aFile.exists() && !aFile.isWritable())
  1528.       return; // XXX
  1529.  
  1530.     // init stream
  1531.     var stream = Cc["@mozilla.org/network/file-output-stream;1"].
  1532.                  createInstance(Ci.nsIFileOutputStream);
  1533.     stream.init(aFile, 0x02 | 0x08 | 0x20, 0600, 0);
  1534.  
  1535.     // utf-8 converter stream
  1536.     var converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
  1537.                  createInstance(Ci.nsIConverterOutputStream);
  1538.     converter.init(stream, "UTF-8", 0, 0x0000);
  1539.  
  1540.     // weep over stream interface variance
  1541.     var streamProxy = {
  1542.       converter: converter,
  1543.       write: function(aData, aLen) {
  1544.         this.converter.writeString(aData);
  1545.       }
  1546.     };
  1547.  
  1548.     // query places root
  1549.     var options = this.history.getNewQueryOptions();
  1550.     options.expandQueries = false;
  1551.     var query = this.history.getNewQuery();
  1552.     query.setFolders([this.bookmarks.placesRoot], 1);
  1553.     var result = this.history.executeQuery(query, options);
  1554.     result.root.containerOpen = true;
  1555.     // serialize as JSON, write to stream
  1556.     this.serializeNodeAsJSONToOutputStream(result.root, streamProxy,
  1557.                                            false, false, aExcludeItems);
  1558.     result.root.containerOpen = false;
  1559.  
  1560.     // close converter and stream
  1561.     converter.close();
  1562.     stream.close();
  1563.   },
  1564.  
  1565.   /**
  1566.    * ArchiveBookmarksFile()
  1567.    *
  1568.    * Creates a dated backup once a day in <profile>/bookmarkbackups.
  1569.    * Stores the bookmarks using JSON.
  1570.    *
  1571.    * @param int aNumberOfBackups - the maximum number of backups to keep
  1572.    *
  1573.    * @param bool aForceArchive - forces creating an archive even if one was 
  1574.    *                             already created that day (overwrites)
  1575.    */
  1576.   archiveBookmarksFile:
  1577.   function PU_archiveBookmarksFile(aNumberOfBackups, aForceArchive) {
  1578.     // get/create backups directory
  1579.     var dirService = Cc["@mozilla.org/file/directory_service;1"].
  1580.                      getService(Ci.nsIProperties);
  1581.     var bookmarksBackupDir = dirService.get("ProfD", Ci.nsILocalFile);
  1582.     bookmarksBackupDir.append("bookmarkbackups");
  1583.     if (!bookmarksBackupDir.exists()) {
  1584.       bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
  1585.       if (!bookmarksBackupDir.exists())
  1586.         return; // unable to create directory!
  1587.     }
  1588.  
  1589.     // construct the new leafname
  1590.     // Use YYYY-MM-DD (ISO 8601) as it doesn't contain illegal characters
  1591.     // and makes the alphabetical order of multiple backup files more useful.
  1592.     var date = new Date().toLocaleFormat("%Y-%m-%d");
  1593.     var backupFilename = this.getFormattedString("bookmarksArchiveFilename", [date]);
  1594.  
  1595.     var backupFile = null;
  1596.     if (!aForceArchive) {
  1597.       var backupFileNames = [];
  1598.       var backupFilenamePrefix = backupFilename.substr(0, backupFilename.indexOf("-"));
  1599.       var entries = bookmarksBackupDir.directoryEntries;
  1600.       while (entries.hasMoreElements()) {
  1601.         var entry = entries.getNext().QueryInterface(Ci.nsIFile);
  1602.         var backupName = entry.leafName;
  1603.         if (backupName.substr(0, backupFilenamePrefix.length) == backupFilenamePrefix) {
  1604.           if (backupName == backupFilename)
  1605.             backupFile = entry;
  1606.           backupFileNames.push(backupName);
  1607.         }
  1608.       }
  1609.  
  1610.       var numberOfBackupsToDelete = 0;
  1611.       if (aNumberOfBackups > -1)
  1612.         numberOfBackupsToDelete = backupFileNames.length - aNumberOfBackups;
  1613.  
  1614.       if (numberOfBackupsToDelete > 0) {
  1615.         // If we don't have today's backup, remove one more so that
  1616.         // the total backups after this operation does not exceed the
  1617.         // number specified in the pref.
  1618.         if (!backupFile)
  1619.           numberOfBackupsToDelete++;
  1620.  
  1621.         backupFileNames.sort();
  1622.         while (numberOfBackupsToDelete--) {
  1623.           let backupFile = bookmarksBackupDir.clone();
  1624.           backupFile.append(backupFileNames[0]);
  1625.           backupFile.remove(false);
  1626.           backupFileNames.shift();
  1627.         }
  1628.       }
  1629.  
  1630.       // do nothing if we either have today's backup already
  1631.       // or the user has set the pref to zero.
  1632.       if (backupFile || aNumberOfBackups == 0)
  1633.         return;
  1634.     }
  1635.  
  1636.     backupFile = bookmarksBackupDir.clone();
  1637.     backupFile.append(backupFilename);
  1638.  
  1639.     if (aForceArchive && backupFile.exists())
  1640.         backupFile.remove(false);
  1641.  
  1642.     if (!backupFile.exists())
  1643.       backupFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
  1644.  
  1645.     this.backupBookmarksToFile(backupFile);
  1646.   },
  1647.  
  1648.   /**
  1649.    * Get the most recent backup file.
  1650.    * @returns nsIFile backup file
  1651.    */
  1652.   getMostRecentBackup: function PU_getMostRecentBackup() {
  1653.     var dirService = Cc["@mozilla.org/file/directory_service;1"].
  1654.                      getService(Ci.nsIProperties);
  1655.     var bookmarksBackupDir = dirService.get("ProfD", Ci.nsILocalFile);
  1656.     bookmarksBackupDir.append("bookmarkbackups");
  1657.     if (!bookmarksBackupDir.exists())
  1658.       return null;
  1659.  
  1660.     var backups = [];
  1661.     var entries = bookmarksBackupDir.directoryEntries;
  1662.     while (entries.hasMoreElements()) {
  1663.       var entry = entries.getNext().QueryInterface(Ci.nsIFile);
  1664.       if (!entry.isHidden() && entry.leafName.match(/^bookmarks-.+(html|json)?$/))
  1665.         backups.push(entry.leafName);
  1666.     }
  1667.  
  1668.     if (backups.length ==  0)
  1669.       return null;
  1670.  
  1671.     backups.sort();
  1672.     var filename = backups.pop();
  1673.  
  1674.     var backupFile = bookmarksBackupDir.clone();
  1675.     backupFile.append(filename);
  1676.     return backupFile;
  1677.   }
  1678. };
  1679.